import _ from 'lodash';
import * as d3 from 'd3';
import * as topojson from 'topojson-client';
import {
    formatScale,
    getCenter,
    toEarthReverse,
    toSkyReverse,
    transGeoDataForSky,
    transToGeoData
} from "@/utils/projection/projectionTools.js";

const _FONT_FAMILY = "Arial, 'Microsoft YaHei', Sans-serif";

let _svgIds = [];
let _clickTimeoutId = null;
let _clickNum = 0;

function handleGlobalClick(single, double, triple, timeout, event) {
    // if (typeof single !== 'function' || typeof double !== 'function' || typeof triple !== 'function') {
    //     throw 'handleGlobalClick: callback must be functions';
    // }
    clearTimeout(_clickTimeoutId);
    _clickNum += 1;
    _clickTimeoutId = setTimeout(() => {
        if (_clickNum === 1 && single) {
            single(event);
        } else if (_clickNum === 2 && double) {
            double(event);
        } else if (_clickNum === 3 && triple) {
            triple(event);
        }
        _clickNum = 0;
    }, timeout);
}

function handleEleClick(d3Obj, projection, event, data) {
    console.log('Clicked: ' + d3Obj.attr('d3-id'));
}

function handleEleMouseOn(d3Obj, projection, event, data) {
    // console.log('MouseOn: ' + d3Obj.attr('name'));
}

function handleEleMouseOut(d3Obj, projection, event) {
    // console.log('MouseOut: ' + d3Obj.attr('name'));
}

const custDraw = function (projection, d, d3Obj) {
    if (!d) {
        console.log('no d')
    }
    let path = d3.geoPath().projection(projection);
    if (d3Obj.attr('d3-gen') === 'path') {
        d3Obj.classed('d3-gen-path', true);
        path = path.pointRadius(d.properties ? d.properties.radius || 1 : 1);
        return path(d);
    } else if (d3Obj.attr('d3-gen') === 'point') {
        d3Obj.classed('d3-gen-point', true);
        return path(d3.geoCircle().radius(d.properties ? d.properties.radius || 1 : 1).center(d.coordinates || d.geometry.coordinates)());
    } else if (d3Obj.attr('d3-gen') === 'xy') {
        d3Obj.classed('d3-gen-xy', true);
        let coord = d.coordinates || d.geometry.coordinates;
        // if (!d.type) {
        //     d.type = 'Point';
        // }
        let p = path(d);
        let xy = projection(coord);
        let x = '';
        let y = '';
        // console.log(coord, p, xy);
        if (p && xy) {
            [x, y] = xy;
        }
        d3Obj.attr('x', x);
        d3Obj.attr('y', y);
        return [x, y];
    }
}

const reRender = (projection, d3Svg) => {
    d3Svg.selectAll('.d3-gen-path, .d3-gen-point').attr('d', function (d) {
        let that = d3.select(this);
        return custDraw(projection, d, that);
    });
    d3Svg.selectAll('.d3-gen-xy').attr('d3-xy', function (d) {
        let that = d3.select(this);
        return custDraw(projection, d, that);
    });
}

class ProjectionAbst {
    constructor(zoneDom, options) {
        if (!zoneDom) {
            throw 'svg zone not found';
        }
        this.zoneDom = zoneDom;
        this.options = options || {};
        // ====================================
        this._init_properties();
        // ====================================
        this._init();
        this._mountEvents();
    }

    _init_properties() {
        this.svgId = null;
        this.svg = null;
        this.tooltip = null;
        this.projection = null;
        this.defaultOpt = {};
        // ====================================
        this.resources = [];
        this.loaded = false;
        this.destroyed = false;
    }

    _init() {
    }

    renderPath() {
    }

    _renderBase() {
    }

    renderPoints() {
    }

    renderLines() {
    }

    renderPolygons() {
    }

    renderGeos() {
    }

    _mountEvents() {
    }

    async setup() {
        await this._renderBase();
        this.loaded = true;
    }

    _destroy() {
    }

    async destroy() {
        await this._destroy();
        this.destroyed = true;
    }

}

export class Projection extends ProjectionAbst {
    _init_properties() {
        super._init_properties();
        // ====================================
        this.reflectX = false;
        this.reflectY = false;
        // ====================================
        this.levels = ['base', 'hot'];
        this.graticule = null;
        this.zoomSize = 1;
        this.dragFactor = [1, -1];  //补偿系数
        this.increaseFactor = [1, 1, -1]; // 补偿系数
    }

    _setDefaultOpt() {
        this.defaultOpt.svgWidth = '355';
        this.defaultOpt.svgHeight = '300';
        this.defaultOpt.levelSteps = 3;
        this.defaultOpt.svgBgColor = '#F0F8FF';                 // B0E0E6 B0C4DE F0FFFF F0F8FF
        this.defaultOpt.projBgColor = 'transparent';
        this.defaultOpt.projGridColor = 'transparent';          // transparent
        this.defaultOpt.projGridTextColor = 'grey';
        this.defaultOpt.textBgColor = 'white';
        this.defaultOpt.textColor = 'black';
        // ====================================
        this.defaultOpt.xRange = [-180, 180];
        this.defaultOpt.yRange = [-90, 90];
        // ====================================
        this.defaultOpt.scaleRatio = Math.PI * 0.8;
        this.defaultOpt.center = [0, 0];
        this.defaultOpt.rotate = [0, 0, 0];
        // ====================================
        this.defaultOpt.zoom = true;
        this.defaultOpt.zoomScale = [0.5, 10];
        // ====================================
        this.defaultOpt.drag = true;
        this.defaultOpt.dragRatio = 5; // 值越小，动得越快
        // ====================================
        this.defaultOpt.rotation = false;
        this.defaultOpt.rotationDirection = [1, 1, 1]; // 默认情况下，x => 向右；y => 向上；z => 顺时针
        this.defaultOpt.rotationRatio = 360 / (24 * 60 * 60);
        this.defaultOpt.rotationFrequency = 25;
        // ====================================
        this.defaultOpt.clickInterval = 225;
    }

    _mergeOptions() {
        this.options = _.merge(this.defaultOpt, this.options);
    }

    _init() {
        this._initOptions();
        this._initSvg();
        this._initTooltip();
        this._initProjection();
        this._initGraticule();
    }

    _initOptions() {
        this._setDefaultOpt();
        this._mergeOptions();
    }

    _initSvg() {
        let self = this;
        self.svg = d3.select(self.zoneDom).select('svg');
        if (self.svg.empty()) {
            self.svg = d3.select(self.zoneDom).append('svg');
        }
        if (self.svg.attr('d3-loaded') === 'true') {
            throw 'svg already loaded';
        } else {
            self.svg.attr('d3-loaded', 'true');
        }
        self.svgId = 'svg-proj-' + (_svgIds.length + 1);
        _svgIds.push(self.svgId);
        // console.log(this.svgId, this.options);
        self.svg.attr('d3-id', self.svgId);
        // ==========================================================
        // self.svg.style('min-width', self.options.svgWidth).style('min-height', self.options.svgHeight);
        self.svg.attr('width', '100%').style('min-height', '350px');
        // self.svg.attr('viewBox', '0,0,500,500').attr('preserveAspectRatio', 'xMinYMid meet');
        self.svg.style('background-color', self.options.svgBgColor);
        // ==========================================================
        // 初始化画布，定好层级
        // ==========================================================
        self.svg.append('g').classed('bottom', true);
        let steps = _.range(1, self.options.levelSteps + 1);
        self.levels.forEach(level => {
            steps.forEach(step => {
                self.svg.append('g').classed(level + '-' + step, true);
            })
        })
        self.svg.append('g').classed('top', true);
        // ==========================================================
    }

    _creatTooltip(tooltipId) {
        let self = this;
        // let zone = d3.select(self.zoneDom);
        let zone = d3.select('body'); //
        // let tooltip = zone.select('div[d3-id="' + tooltipId + '"]');
        let tooltip = zone.select('#' + tooltipId);
        if (!tooltip.empty()) {
            return tooltip;
        }
        return zone.append("div")
            // .attr("d3-id", tooltipId)
            .attr("id", tooltipId)
            .style("position", "absolute")
            .style('width', 'auto')
            .style('height', 'auto')
            .style("background-color", self.options.textBgColor)
            .style("border", "thin solid grey")
            .style('border-radius', '1.5px')
            .style("padding", "0.5px 1.5px")
            .style("opacity", 0.7)
            .style("font-family", _FONT_FAMILY)
            .style("font-size", "12px")
            .style("font-weight", "bold")
            .style("font", self.options.textColor)
            .style("visibility", "hidden");
    }

    _initTooltip() {
        let tooltipId = _.uniqueId(this.svgId + '-tooltip' + '-');
        this.tooltip = this._creatTooltip(tooltipId);
    }

    _setProjection() {
    }

    _tweakProjection() {
        this.projection
            .center(this.options.center)
            .rotate(this.options.rotate)
            .scale(formatScale(this.svg.node(), this.options.scaleRatio) * this.zoomSize)
            .translate(getCenter(this.svg.node()))
            .reflectX(this.reflectX)
            .reflectY(this.reflectY)
    }

    _initProjection() {
        this.projection = this._setProjection();
        this._tweakProjection()
    }

    _setGraticule() {
        return d3.geoGraticule();
    }

    _tweakGraticule() {
    }

    _initGraticule() {
        this.graticule = this._setGraticule();
        this._tweakGraticule()
    }

    async _getJson(url) {
        let data;
        try {
            data = await d3.json(url);
        } catch (e) {
            console.log('error: failed to load data');
            return;
        }
        return data;
    }


    _showTooltip(x, y, text) {
        this.tooltip.text(text)
            .style("left", x + 'px')
            .style("top", y + 'px')
            .style("visibility", "visible");
    }

    _hideTooltip() {
        this.tooltip.style("visibility", "hidden");
    }

    _mountPathProperties(d3Obj, p, cls) {
        let self = this;
        d3Obj
            .attr('stroke', p.stroke || 'black')
            .attr('stroke-width', p['stroke-width'] || 1)
            .attr("fill", p.fill || 'none')
            .attr('opacity', p.opacity || 1)
            .attr('name', p.name || '')
            .attr('d3-id', function () {
                let that = d3.select(this);
                if (p.id) {
                    that.attr('id', p.id);
                }
                return p.id || _.uniqueId(self.svgId + '-' + cls + '-');
            })
            .attr('d3-attr', function () {
                let that = d3.select(this);
                let attrs = p.attrs || {};
                for (let key in attrs) {
                    that.attr(key, attrs[key]);
                }
            })
            .attr('d3-style', function () {
                let that = d3.select(this);
                let styles = p.styles || {};
                for (let key in styles) {
                    that.style(key, styles[key]);
                }
            });
    }

    renderPath(level, loadType, data, genType, cls) { // loadType: datum / data ; genType: path / point
        let self = this;
        let zone = self.svg.select('g.' + level);
        if (zone.empty()) {
            throw 'level error';
        }
        loadType = loadType || 'data';
        if (!(loadType === 'datum' || loadType === 'data')) {
            throw 'loadType error';
        }
        if (!data) {
            throw 'no data';
        }
        genType = genType || 'path';
        if (!(genType === 'point' || genType === 'path')) {
            throw 'genType error';
        }
        let render = zone.append('g').classed('d3-g-' + cls, true);
        if (loadType === 'datum') {
            render = render.append("path").datum(data).attr('d3-gen', genType).classed(cls, true).attr('d', function (d) {
                let that = d3.select(this);
                return custDraw(self.projection, d, that);
            });
        } else { // loadType === 'data'
            render = render.selectAll('path').data(data).enter().append("path").attr('d3-gen', genType).classed(cls, true).attr('d', function (d) {
                let that = d3.select(this);
                return custDraw(self.projection, d, that);
            });
        }
        render.attr('d3-ps', function (d) {
            let that = d3.select(this);
            if (d.properties) {
                self._mountPathProperties(that, d.properties, cls)
            }
        });
        return render;
    }

    _mountTextProperties(d3Obj, p, cls) {
        let self = this;
        d3Obj
            .attr('dx', p.dx || 0)
            .attr('dy', p.dy || 0)
            .text(p.text || '')
            .attr('text-anchor', p['text-anchor'] || 'middle')
            .attr('font-family', p['font-family'] || _FONT_FAMILY)
            .attr('font-size', p['font-size'] || '12px')
            .attr('stroke', p.stroke || 'black')
            .attr('stroke-width', p['stroke-width'] || 0)
            .attr("fill", p.fill || 'none')
            .attr('opacity', p.opacity || 1)
            .attr('name', p.name || '')
            .attr('d3-id', function () {
                let that = d3.select(this);
                if (p.id) {
                    that.attr('id', p.id);
                }
                return p.id || _.uniqueId(self.svgId + '-' + cls + '-');
            })
            .attr('d3-attr', function () {
                let that = d3.select(this);
                let attrs = p.attrs || {};
                for (let key in attrs) {
                    that.attr(key, attrs[key]);
                }
            })
            .attr('d3-style', function () {
                let that = d3.select(this);
                let styles = p.styles || {};
                for (let key in styles) {
                    that.style(key, styles[key]);
                }
            });
    }

    renderText(level, data, cls) {
        let self = this;
        let zone = self.svg.select('g.' + level);
        if (zone.empty()) {
            throw 'level error';
        }
        if (!data) {
            throw 'no data';
        }
        let genType = 'xy';
        let render = zone.append('g').classed('d3-g-' + cls, true);
        render = render.selectAll('text').data(data).enter().append('text').attr('d3-gen', genType).classed(cls, true).attr('d3-xy', function (d) {
            let that = d3.select(this);
            return custDraw(self.projection, d, that);
        });
        render.attr('d3-ps', function (d) {
            let that = d3.select(this);
            if (d.properties) {
                self._mountTextProperties(that, d.properties, cls)
            }
        });
        return render;
    }


    renderCommonText(level, data, cls) {
        data = transToGeoData(data);
        return this.renderText(level, data, cls);
    }


    _renderBackground(bg_color, level, optical) {
        // console.log('render background')
        let self = this;
        self.options.projBgColor = bg_color || self.options.projBgColor;
        level = level || 'base-1';
        let loadType = 'datum';
        let data = {type: "Sphere"};
        let genType = 'path';
        let cls = 'bg';
        let base = self.renderPath(level, loadType, data, genType, cls);
        base = base
            .attr('stroke', 'grey')
            .attr('stroke-width', 0.1)
            .attr('fill', self.options.projBgColor) //87ceeb ADD8E6
            .attr('opacity', optical || 1)
        return base;
    }

    _genXAxisText(x) {
        // return `${Math.abs(x)}° ${(x < 0 && x !== -180) ? 'W' : (x === 0 || x === 180 || x === -180) ? '' : 'E'}`;
        let abs = Math.abs(x);
        return (abs === 0 || abs === 180) ? abs : x < 0 ? abs + '°W' : abs + '°E';
    }

    _genYAxisText(y) {
        // return `${Math.abs(y)}° ${y < 0 ? 'S' : y === 0 ? '' : 'N'}`;
        let abs = Math.abs(y);
        return y < 0 ? abs + '°S' : y === 0 ? '' : abs + '°N';
    }

    _genAxisData() {
        let self = this;
        let step = self.graticule.step();
        let gap = 1;
        let xs = _.range(self.options.xRange[0], self.options.xRange[1] + gap, gap);
        let ys = _.range(self.options.yRange[0], self.options.yRange[1] + gap, gap);
        let xData = [];
        let yData = [];
        xs.forEach(x => {
            if (x % (step[0] * 3) === 0) {
                xData.push(x);
            }
        });
        ys.forEach(y => {
            if (y % (step[1] * 3) === 0) {
                yData.push(y);
            }
        })
        return [xData, yData];
    }

    _renderXAxis(xData, level) {
        let self = this;
        let delt = 5;
        let ds = [];
        let ts = [];
        xData.forEach(x => {
            let d = {
                'coordinates': [[x, -delt / 2], [x, delt / 2]],
                'properties': {
                    'name': 'XAxis',
                    'text': self._genXAxisText(x),
                    'stroke': self.options.projGridColor,
                    'stroke-width': 1.5,
                    'opacity': 0.7,
                    // 'attrs': {
                    //     'filter': 'drop-shadow(0 0 5px ' + self.options.projGridColor + ')'
                    // }
                }
            }
            let t = {
                'coordinates': [x, 0],
                'properties': {
                    'name': 'XAxisText',
                    'text': self._genXAxisText(x),
                    'font-size': '10px',
                    'stroke': self.options.projGridColor,
                    'stroke-width': 0,
                    'fill': self.options.projGridTextColor,
                    'opacity': 1,
                }
            }
            ds.push(d);
            ts.push(t);
        });
        return [self.renderLines(ds, level, 'xAxis', false, false, false), self.renderCommonText('top', ts, 'xAxisText')];
    }


    _renderYAxis(yData, level) {
        let self = this;
        let delt = 5;
        let ds = [];
        let ts = [];
        yData.forEach(y => {
            let d = {
                'coordinates': [[-delt / 2, y], [delt / 2, y]],
                'properties': {
                    'name': 'YAxis',
                    'text': self._genYAxisText(y),
                    'stroke': self.options.projGridColor,
                    'stroke-width': 1.5,
                    'opacity': 0.7,
                    'attrs': {
                        'filter': 'drop-shadow(0 0 3px ' + self.options.projGridColor + ')'
                    }
                }
            }
            let t = {
                'coordinates': [delt / 2, y],
                'properties': {
                    'name': 'YAxisText',
                    'text': self._genYAxisText(y),
                    'font-size': '10px',
                    'stroke': self.options.projGridColor,
                    'stroke-width': 0,
                    'fill': self.options.projGridTextColor,
                    'opacity': 1,
                }
            }
            ds.push(d);
            ts.push(t);
        });
        return [self.renderLines(ds, level, 'yAxis', false, false, false), self.renderCommonText('top', ts, 'yAxisText')];
    }

    _renderGrid(gridData, level) {
        // console.log('render grid')
        let self = this;
        self.graticule = gridData || self.graticule;
        gridData = self.graticule;
        level = 'base-2' || level;
        let loadType = 'datum';
        let genType = 'path';
        let cls = 'grid';
        let base = self.renderPath(level, loadType, gridData, genType, cls);
        base = base
            .attr('stroke', self.options.projGridColor) // f5f5f5 C0C0C0
            .attr('stroke-width', 0.5)
            .attr('fill', 'none');
        let [xData, yData] = self._genAxisData();
        let xAxis = self._renderXAxis(xData, level);
        let yAxis = self._renderYAxis(yData, level);
        return [base, xAxis, yAxis];
    }

    _mountEleClick(d3Obj, clickCallback) {
        if (clickCallback === false) {
            return;
        }
        let _clickCallback = clickCallback || handleEleClick;
        d3Obj.on('click', function (e) {
            _clickCallback(d3Obj, e);
            // e.stopPropagation();
        });
    }

    _showTooltipWithMouse(e, d) {
        let text = d.properties ? d.properties.text || '' : '';
        let x = e.pageX + 15;
        let y = e.pageY;
        if (!text) {
            return;
        }
        this._showTooltip(x, y, text);
    }

    _mountEleMouse(d3Obj, mouseOnCallback, mouseOutCallback) {
        if (mouseOnCallback === false || mouseOutCallback === false) {
            return;
        }
        let self = this;
        let _mouseOnCallback = mouseOnCallback || handleEleMouseOn;
        let _mouseOutCallback = mouseOutCallback || handleEleMouseOut;
        d3Obj
            .on("mouseenter", function (e, d) { // mouseover
                // console.log('move in ' + d.properties.text);
                self._showTooltipWithMouse(e, d);
                _mouseOnCallback(d3Obj, self, e, d);
            })
            .on("mousemove", function (e, d) { // mousemove
                // console.log('move on ' + d.properties.text, e)
                self._showTooltipWithMouse(e, d);
            })
            .on("mouseleave", function (e, d) { // mouseout
                // console.log('move out ' + d.properties.text);
                self._hideTooltip();
                _mouseOutCallback(d3Obj, self, e, d);
            });
    }

    renderGeos(geos, level, cls) {
        if (!geos) {
            throw 'no geos'
        }
        let self = this;
        level = level || 'hot-1';
        let loadType = 'data';
        let data = geos;
        let genType = 'path';
        cls = cls || 'geo';
        return self.renderPath(level, loadType, data, genType, cls);
    }

    renderCommon(commData, level, cls, genType, clickCallback, mouseOnCallback, mouseOutCallback) {
        if (!commData) {
            throw 'no common data'
        }
        let self = this;
        level = level || 'hot-1';
        let loadType = 'data';
        let data = transToGeoData(commData);
        genType = genType || 'path';
        cls = cls || 'common';
        let base = self.renderPath(level, loadType, data, genType, cls);
        self._mountEleClick(base, clickCallback);
        self._mountEleMouse(base, mouseOnCallback, mouseOutCallback);
        return base;
    }


    renderPoints(pointsData, level, cls, clickCallback, mouseOnCallback, mouseOutCallback) {
        // console.log('render points')
        let self = this;
        level = level || 'hot-3';
        cls = cls || 'point';
        return self.renderCommon(pointsData, level, cls, 'point', clickCallback);
    }

    renderLines(linesData, level, cls, clickCallback, mouseOnCallback, mouseOutCallback) {
        // console.log('render lines')
        let self = this;
        level = level || 'hot-3';
        cls = cls || 'line';
        return self.renderCommon(linesData, level, cls, 'path', clickCallback, mouseOnCallback, mouseOutCallback);
    }

    renderPolygons(polygonsData, level, cls, clickCallback, mouseOnCallback, mouseOutCallback) {
        // console.log('render polygons')
        let self = this;
        level = level || 'hot-3';
        cls = cls || 'polygon';
        return self.renderCommon(polygonsData, level, cls, 'path', clickCallback, mouseOnCallback, mouseOutCallback);
    }

    remove(d3Obj) {
        d3Obj.select(function () {
            return this.parentNode;
        }).remove();
    }

    _destroy() {
        this.resources.forEach(v => {
            if (v.val) {
                if (v.cls === 'Interval') {
                    clearInterval(v.val);
                }
                if (v.cls === 'Timeout') {
                    clearTimeout(v.val);
                }
                if (v.cls === 'AnimationFrame') {
                    cancelAnimationFrame(v.val);
                }
            }
        });
        this.svg.remove();
        this.tooltip.remove();
    }

    _reRender() {
        reRender(this.projection, this.svg);
    }

    _resizeProjConf() {
        let self = this;
        let proj = self.projection;
        // proj = proj.scale(formatScale(self.svg.node(), self.options.scaleRatio) * self.zoomSize);
        self.projection = proj.scale(formatScale(self.svg.node(), self.options.scaleRatio) * self.zoomSize).translate(getCenter(self.svg.node()));
    }

    _translateProjConf(x, y) {
        let self = this;
        let proj = self.projection;
        let c = getCenter(self.svg.node());
        self.projection = proj.translate([proj.translate()[0] - (x - c[0]), proj.translate()[1] - (y - c[1])]);
    }

    _resetProjConf() {
        let self = this;
        let proj = self.projection;
        let c = getCenter(self.svg.node());
        self.zoomSize = 1;
        self.projection = proj
            .rotate(self.options.rotate)
            .scale(formatScale(self.svg.node(), self.options.scaleRatio) * self.zoomSize)
            .translate(c);
    }

    _zoomProjConf() {
        let self = this;
        let proj = self.projection;
        self.projection = proj.scale(formatScale(self.svg.node(), self.options.scaleRatio) * self.zoomSize);
    }

    _dragProjConf(deltX, deltY) {
        let self = this;
        let proj = self.projection;
        self.projection = proj.rotate([proj.rotate()[0] + deltX * self.dragFactor[0] * self.increaseFactor[0], proj.rotate()[1] + deltY * self.dragFactor[1] * self.increaseFactor[1], proj.rotate()[2]]);
    }

    _rotationProjConf(deltDeg) {
        let self = this;
        let proj = self.projection;
        self.projection = proj.rotate([proj.rotate()[0] + deltDeg * self.options.rotationDirection[0] * self.increaseFactor[0], proj.rotate()[1], proj.rotate()[2]]);
    }

    _mountResize() {
        let self = this;
        // console.log(self.svgId + ': mount resize');
        let resize = () => {
            // console.log(self.svgId + ': resize');
            self._resizeProjConf();
            self._reRender();
        }

        resize();
        let zone = d3.select(self.zoneDom);
        let lastWidth = zone.node().clientWidth;
        let lastHeight = zone.node().clientHeight;

        let anim = {
            cls: 'AnimationFrame', val: null
        };

        function check() {
            let curWidth = zone.node().clientWidth;
            let curHeight = zone.node().clientHeight;
            if (curWidth !== lastWidth || curHeight !== lastHeight) {
                resize();
                lastWidth = curWidth;
                lastHeight = curHeight;
            }
            anim.val = requestAnimationFrame(check);
        }

        // check();
        anim.val = requestAnimationFrame(check);

        self.resources.push(anim);
    }

    _mountClick() {
        let self = this;

        // console.log(self.svgId + ': mount click');

        function translate(e) {
            let x = e.offsetX, y = e.offsetY;
            self._translateProjConf(x, y);
            self._reRender();
        }

        function reset(e) {
            self._resetProjConf();
            self._reRender();
        }

        self.svg.on('click', function (e) {
            handleGlobalClick(null, translate, reset, self.options.clickInterval, e);
        });
    }

    _mountZoom() {
        let self = this;
        // console.log(self.svgId + ': mount zoom');
        let zoom = d3.zoom()
            .scaleExtent(self.options.zoomScale)
            .on('zoom', function (e) {
                // console.log(self.svgId + ': zoom');
                // console.log(e);
                self.zoomSize = e.transform.k;
                self._zoomProjConf();
                self._reRender();
            }).filter((event) => {
                // console.log(event);
                return event.type === 'wheel';
            });
        self.svg.call(zoom);
    }

    _mountDrag() {
        let self = this;
        // console.log(self.svgId + ': mount drag');
        let drag = d3.drag()
            .on('drag', function (e) {
                // console.log(self.svgId + ': drag');
                // console.log(e);
                let deltX = e.dx / (self.options.dragRatio * self.zoomSize);
                let deltY = e.dy / (self.options.dragRatio * self.zoomSize);
                // console.log(deltX, deltY);
                self._dragProjConf(deltX, deltY);
                self._reRender();
            })
        self.svg.call(drag);
    }

    _mountRotation() {
        let self = this;
        // console.log(self.svgId + ': mount rotation');
        let interval = 1000 / self.options.rotationFrequency;
        let bt = new Date().getTime();

        let anim = {
            cls: 'AnimationFrame', val: null
        };

        function render() {
            console.log(self.svgId + ': rotation');
            let nt = new Date().getTime();
            let delt = nt - bt;
            if (delt >= interval) {
                bt = nt;
                let deltDeg = delt * self.options.rotationRatio;
                self._rotationProjConf(deltDeg);
                self._reRender();
            }
            anim.val = requestAnimationFrame(render);
        }

        // render();
        anim.val = requestAnimationFrame(render);

        self.resources.push(anim);
    }

    _mountEvents() {
        let self = this;
        self._mountResize();
        self._mountClick();
        if (self.options.drag) {
            self._mountDrag();
        }
        if (self.options.rotation) {
            self._mountRotation();
        }
        if (self.options.zoom) {
            self._mountZoom();
        }
    }

    _renderBase() {
        this._renderBackground();
        this._renderGrid();
        // others
    }

}

export class ProjectionAstro extends Projection {


    _setDefaultOpt() {
        super._setDefaultOpt();
        this.defaultOpt.sun_moon = true;
        this.defaultOpt.sun_interval = 10;
        this.defaultOpt.moon_interval = 10;
        // =================================================================
        this.defaultOpt.switch_to_pos = 0; // 0: [0,0]; 1: sun; 2:moon
        // =================================================================
        this.defaultOpt.sun_color = 'gold';
        this.defaultOpt.sun_trace = false;
        this.defaultOpt.sun_zone_radius = 0;
        this.defaultOpt.sun_zone_color = 'none';
        // =================================================================
        this.defaultOpt.moon_color = 'white';
        this.defaultOpt.moon_trace = false;
        this.defaultOpt.moon_zone_radius = 0;
        this.defaultOpt.moon_zone_color = 'none';
    }

    async _getSunPos() {
        let self = this;
        let data = await self._getJson('https://svom-gwacn.cn/tools/cal/getSunTerr');
        if (data && data.code == 0) {
            return data.data;
        }
    }

    async _getMoonPos() {
        let self = this;
        let data = await self._getJson('https://svom-gwacn.cn/tools/cal/getMoonTerr');
        if (data && data.code == 0) {
            return data.data;
        }
    }


    #sunTimer = new Date().getTime();

    _renderSun(pos) {
        let self = this;
        // =================================================
        self.svg.selectAll('.sun').select(function () {
            return this.parentNode;
        }).remove();
        self.svg.selectAll('.sun-zone').select(function () {
            return this.parentNode;
        }).remove();
        // =================================================
        if (pos) {
            let zonePos = [pos[0], pos[1]];
            let zone = {
                'coordinates': zonePos, 'properties': {
                    'name': 'SunZone',
                    'fill': self.options.sun_zone_color,
                    'stroke': 'none',
                    'stroke-width': 0,
                    'radius': self.options.sun_zone_radius,
                    'opacity': 0.25
                }
            };
            let sun = {
                'coordinates': pos, 'properties': {
                    'name': 'Sun',
                    'text': 'Sun',
                    'stroke': 'grey',
                    'stroke-width': 0.5,
                    'fill': self.options.sun_color,
                    'radius': 3,
                    'opacity': 1
                }
            };
            if (self.options.sun_zone_radius > 0) {
                self.renderPoints([zone], 'base-2', 'sun-zone', null, false, false);
            }
            self.renderPoints([sun], 'base-3', 'sun');

            let trace = {
                'coordinates': pos, 'properties': {
                    'name': 'SunTrace',
                    'text': 'Sun',
                    'stroke': 'none',
                    'stroke-width': 0,
                    'fill': self.options.sun_color,
                    'radius': 0.8,
                    'opacity': 0.5
                }
            };
            if (self.sun_trace && new Date().getTime() - self.#sunTimer > self.options.sun_interval * 1000 * 60) {
                self.renderPoints([trace], 'base-3', 'sun-trace');
                self.#sunTimer = new Date().getTime();
            }
        }
    }


    #moonTimer = new Date().getTime();

    _renderMoon(pos) {
        let self = this;
        // =================================================
        self.svg.selectAll('.moon').select(function () {
            return this.parentNode;
        }).remove();
        self.svg.selectAll('.moon-zone').select(function () {
            return this.parentNode;
        }).remove();
        // =================================================
        if (pos) {
            let zonePos = [pos[0], pos[1]];
            let zone = {
                'coordinates': zonePos, 'properties': {
                    'name': 'MoonZone',
                    'fill': self.options.moon_zone_color,
                    'stroke': 'none',
                    'stroke-width': 0,
                    'radius': self.options.moon_zone_radius,
                    'opacity': 0.3
                }
            };
            let moon = {
                'coordinates': pos, 'properties': {
                    'name': 'Moon',
                    'text': 'Moon',
                    'stroke': '#c0c0c0',
                    'stroke-width': 0.5,
                    'fill': self.options.moon_color,
                    'radius': 1.8,
                    'opacity': 1
                }
            };
            if (self.options.moon_zone_radius > 0) {
                self.renderPoints([zone], 'base-2', 'moon-zone', false, false, false);
            }
            self.renderPoints([moon], 'base-3', 'moon');

            let trace = {
                'coordinates': pos, 'properties': {
                    'name': 'MoonTrace',
                    'text': 'Moon',
                    'stroke': 'none',
                    'stroke-width': 0,
                    'fill': self.options.moon_color,
                    'radius': 0.5,
                    'opacity': 0.5
                }
            };
            if (self.moon_trace && new Date().getTime() - self.#moonTimer > self.options.moon_interval * 1000 * 60) {
                self.renderPoints([trace], 'base-3', 'moon-trace');
                self.#moonTimer = new Date().getTime();
            }
        }
    }

    _autoRenderSun() {
        let self = this;
        self._getSunPos().then((pos) => {
            self._renderSun(pos);
        });
        const sunRun = setInterval(() => {
            self._getSunPos().then((pos) => {
                self._renderSun(pos);
            });
        }, 1000 * self.options.sun_interval);
        self.resources.push({cls: 'Interval', val: sunRun});
    }

    _autoRenderMoon() {
        let self = this;
        self._getMoonPos().then((pos) => {
            self._renderMoon(pos);
        });
        const moonRun = setInterval(() => {
            self._getMoonPos().then((pos) => {
                self._renderMoon(pos);
            });
        }, 1000 * self.options.moon_interval);
        self.resources.push({cls: 'Interval', val: moonRun});
    }

    _followProjConf() {
        let self = this;
        let proj = self.projection;
        // self.projection = proj.rotate([posX * self.increaseFactor[0] * -1, proj.rotate()[1], proj.rotate()[2]]);
        self.projection = proj.rotate(self.options.rotate);
    }

    _getSunReverse(pos) {
    }

    async rotateTo(posX, posType) {
        if (posType === false) {
            return;
        }
        let self = this;
        if (!posX) {
            let pos;
            switch (posType) {
                case 1:
                    pos = await self._getSunPos();
                    if (pos) {
                        posX = pos[0];
                    }
                    break;
                case 2:
                    pos = await self._getMoonPos();
                    if (pos) {
                        posX = pos[0];
                    }
                    break;
                case 3:
                    pos = await self._getSunPos();
                    if (pos) {
                        pos = self._getSunReverse(pos);
                        posX = pos[0];
                    }
                    break;
                default:
                    posX = self.options.rotate[0];
                    break;
            }
        }
        if (posX) {
            self.options.rotate[0] = -posX;
            self._followProjConf();
            self._reRender();
        }
    }

    async _renderBase() {
        super._renderBase();
        if (this.options.sun_moon) {
            this._autoRenderSun();
            this._autoRenderMoon();
            await this.rotateTo(null, this.options.switch_to_pos);
        }
    }

}


export class ProjectionEarth extends ProjectionAstro {

    _setDefaultOpt() {
        super._setDefaultOpt();
        this.defaultOpt.projBgColor = '#87ceeb';
        this.defaultOpt.projGridColor = '#f5f5f5';
        this.defaultOpt.textColor = 'navy';
        // =================================================
        this.defaultOpt.projBmColor = '#ffe4b5';
        // =================================================
        this.defaultOpt.switch_to_pos = 1;
        this.defaultOpt.sun_zone_radius = 100;
        this.defaultOpt.sun_zone_color = 'white';
        // =================================================
        this.defaultOpt.rotation = true;
        this.defaultOpt.rotationDirection = [1, 1, 1];
        this.defaultOpt.rotationRatio = 360 / (60 * 60 * 24);
    }

    async _getBaseMapData() {
        let self = this;
        let json = await self._getJson('https://fastly.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json');
        if (json) {
            let data;
            try {
                // data = topojson.feature(json, json.objects.countries).features;
                data = topojson.feature(json, json.objects.land).features;
            } catch (e) {
                throw 'error when parse topojson data';
            }
            return data;
        }
    }

    async _renderBaseMap(bm_color, level) {
        // console.log('render base map');
        let self = this;
        bm_color = bm_color || self.options.projBmColor;
        level = level || 'base-1';
        let loadType = 'data';
        let data;
        let genType = 'path';
        let cls = 'bm';
        data = await self._getBaseMapData();
        let base = self.renderPath(level, loadType, data, genType, cls);
        base = base
            .attr('stroke', 'grey')
            .attr('stroke-width', 0.5)
            .attr("fill", bm_color) // f5deb3 ffe4b5
        return base;
    }

    _getSunReverse(pos) {
        return toEarthReverse(pos);
    }

    async _renderBase() {
        this._renderBackground();
        await this._renderBaseMap();
        this._renderGrid();
        if (this.options.sun_moon) {
            this._autoRenderSun();
            this._autoRenderMoon();
            await this.rotateTo(null, this.options.switch_to_pos);
        }
    }
}


export class ProjectionSky extends ProjectionAstro {

    _setDefaultOpt() {
        super._setDefaultOpt();
        this.defaultOpt.projBgColor = 'black';
        this.defaultOpt.projGridColor = 'grey';
        this.defaultOpt.textColor = '#bdb76b';
        // =================================================================
        this.defaultOpt.xRange = [0, 360];
        this.defaultOpt.yRange = [-90, 90];
        // =================================================================
        this.defaultOpt.in_out = 1; // 1  from outside; -1 from inside
        this.defaultOpt.switch_to_pos = 1;
        this.defaultOpt.sun_zone_radius = 90;
        this.defaultOpt.sun_zone_color = '#dcdcdc';
        this.defaultOpt.moon_zone_radius = 30;
        this.defaultOpt.moon_zone_color = '#dcdcdc';
        // =================================================================
        this.defaultOpt.rotation = false;
        this.defaultOpt.rotationDirection = [-1, 1, 1];
        this.defaultOpt.rotationRatio = 360 / (60 * 60 * 24);
    }

    _initOptions() {
        super._initOptions();
        this.reflectX = this.options.in_out === -1;
        let reflectXFlag = this.reflectX ? -1 : 1;
        this.increaseFactor = [this.increaseFactor[0] * this.options.in_out * reflectXFlag, this.increaseFactor[1], this.increaseFactor[2]];
        this.dragFactor = [this.dragFactor[0] * this.options.in_out, this.dragFactor[1]];
    }

    renderPath(level, loadType, data, genType, cls) { // loadType: datum / data
        loadType = loadType || 'data'
        if (loadType === 'data' && data) {
            data = transGeoDataForSky(data);
        }
        return super.renderPath(level, loadType, data, genType, cls);
    }

    _genXAxisText(x) {
        // return `${x}°`;
        return (x === 0 || x === 360) ? '0' : `${x / 15} h`;
    }

    _genYAxisText(y) {
        return y === 0 ? '' : `${y}°`;
    }

    _tweakGraticule() {
        super._tweakGraticule();
        this.graticule.step([15, 10]);
    }

    async _getSunPos() {
        let self = this;
        let data = await self._getJson('https://svom-gwacn.cn/tools/cal/getSunCele');
        if (data && data.code == 0) {
            return data.data;
        }
    }

    async _getMoonPos() {
        let self = this;
        let data = await self._getJson('https://svom-gwacn.cn/tools/cal/getMoonCele');
        if (data && data.code == 0) {
            return data.data;
        }
    }

    async _getEclipticData() {
        let self = this;
        let data = await self._getJson('https://svom-gwacn.cn/tools/cal/getEclipticLine');
        if (data && data.code == 0) {
            return data.data;
        }
    }

    async _getGalacticData() {
        let self = this;
        let data = await self._getJson('https://svom-gwacn.cn/tools/cal/getGalacticPlane?width=10');
        if (data && data.code == 0) {
            return data.data;
        }
    }

    async _renderEcliptic(level) {
        let self = this;
        level = level || 'base-3';
        let data = await self._getEclipticData();
        if (data) {
            let d = {
                'coordinates': data,
                'properties': {
                    'text': 'Ecliptic',
                    'stroke': 'Gold',
                    'stroke-width': 1.5,
                    'opacity': 1
                }
            }
            self.renderLines([d], level);
        }
    }

    async _renderGalactic(level) {
        let self = this;
        level = level || 'base-3';
        let data = await self._getGalacticData();
        if (data) {
            let d = {
                'coordinates': data,
                'properties': {
                    'text': 'Galactic',
                    'stroke': '#708090',
                    'stroke-width': 1.5,
                    'fill': '#708090',
                    'opacity': 0.5
                }
            }
            self.renderCommon([d], level);
        }
    }

    _getSunReverse(pos) {
        return toSkyReverse(pos);
    }

    async _renderBase() {
        await super._renderBase();
        await this._renderEcliptic();
        await this._renderGalactic();
    }

}
